#!/usr/bin/env python
# -*- coding: utf-8 -*-

CRONFILE = '/etc/serverpanel/cron'
NODESFILE = '/etc/serverpanel/nodes'

import string,cgi,time,sys,os,json,hashlib,base64
from os import curdir, sep
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from subprocess import Popen, PIPE

import serverpanel

# mysql
import MySQLdb
from MySQLdb import MySQLError
import urllib
from urllib2 import Request, urlopen, URLError, HTTPError
import threading
from datetime import datetime
import socket

def log(s):
    serverpanel.log(s)

def doGET(nhost, nport, nuser, npass, uri):
    # construimos la url con autentificación
    url = "http://%s:%s%s" % (nhost, nport, uri)
    req = Request(url, None, {'Authorization': 'Basic ' + base64.b64encode( nuser + ':' + npass )})
    log( "GET %s" % url )
    # Open the url
    fu = urlopen(req, None, 60)
    # Obtain data
    result = fu.read()
    fu.close()
    
    return result


def doPOST(nhost, nport, nuser, npass, uri, data):
    result = None
    
    try:
        # construimos la url con autentificación
        url = "http://%s:%s%s" % (nhost, nport, uri)
        req = Request(url, None, {
            'Authorization': 'Basic ' + base64.b64encode( nuser + ':' + npass ),
            'Content-Type': 'application/x-www-form-urlencoded'
        })
        # Open the url
        fu = urlopen(req, urllib.urlencode(data), 60)
        # Obtain data
        result = fu.read()
        fu.close()
        
    except URLError:
        log( "ERROR: url error: %s" % url )
    
    return result
    


'''
Obtiene los nodos de esta botnet
'''
def getNodes():
    nodes = []
    try:
        f = open(NODESFILE, 'r')
        nodes = [line.strip(' \n').split(':') for line in f]
        f.close()
    except IOError:
        log( "ERROR: cannot read nodes file: %s" % NODESFILE ) 

    return nodes


'''
Actualiza uno de los nodos de nuestra botnet
'''
def updateNode(nhost, nport, nuser, npass):
    nodes = getNodes()
    f = open(NODESFILE, 'w')
    for node in nodes:
        if node[0] == nhost:
            node[1] = nport
            node[2] = nuser
            node[3] = npass
        f.write( ':'.join(node) + "\n" )
    f.close()


'''
Guarda información de un nodo de la botnet
'''
def saveNode(nhost, nport, nuser, npass):
    # buscamos el host en nuestra botnet
    for node in getNodes():
        if node[0] == nhost:
            updateNode(nhost, nport, nuser, npass)
            return
    
    # el nodo no está en la lista
    # lo añadimos
    f = open(NODESFILE, 'a')
    f.write("%s:%s:%s:%s\n" % (nhost, nport, nuser, npass))
    f.close()
    
    '''
    # TODO: ...
    # enviamos este cambio a toda la botnet,
    #    menos al host que se está guardando,
    #    menos a nosotros mismos
    for node in getNodes():
        (bhost, bip, bport, buser, bpass) = node
        if bhost != nhost:
            doPOST(bhost, bport, buser, bpass, '/ping/', {
                'host': nhost,
                'ip':   nip,
                'port': nport,
                'user': nuser,
                'pass': npass
            })
    '''
        
    return


class SPUtils():
    def listdirs(self, directory):
        return [f for f in os.listdir(directory) if os.path.isdir(os.path.join(directory, f))]
    
    def compressdir(self, source):
        config = ServerPanelHandler.config
        # ejecutamos tar -c0 {source}
        p1 = Popen([config.TAR_BIN, '-cO', source], stdout=PIPE)
        # ejecutamos bzip2 -c
        p2 = Popen([config.BZIP2_BIN, '-c'], stdin=p1.stdout, stdout=PIPE)
        # devolvemos el .tar.bz2
        return p2.communicate()[0]


class ServerPanelHandler(BaseHTTPRequestHandler):
    
    config = None
    
    '''
    def __init__(self, request, client_address, server):
        print "commands length = %d" % len(self.commands)
        if len(self.commands) == 0:
            self.commands.append(['/db/list/', self.commandDbList])
            self.commands.append(['/db/backup/', self.commandDbBackup])
            self.commands.append(['/www/list/', self.commandWwwList])
            self.commands.append(['/www/backup/', self.commandWwwBackup])
            self.commands.append(['/svn/list/', self.commandSvnList])
            self.commands.append(['/svn/backup/', self.commandSvnBackup])
            self.commands.append(['/system/users/', self.commandSystemUsers])
            self.commands.append(['/backup/store/', self.commandBackupStore])
            self.commands.append(['/cron/list/', self.commandCronList])
            self.commands.append(['/cron/exec/', self.commandCronExec])
        
        print request, client_address, server
            
        BaseHTTPRequestHandler.__init__(self, request, client_address, server)
    '''
    
    def do_GET(self):
        self.commands = [
            ['/help/', self.commandHelp],
            ['/source/', self.commandSource],
            ['/service/', self.commandService],
            ['/db/list/', self.commandDbList],
            ['/db/backup/', self.commandDbBackup],
            ['/www/list/', self.commandWwwList],
            ['/www/backup/', self.commandWwwBackup],
            ['/svn/list/', self.commandSvnList],
            ['/svn/backup/', self.commandSvnBackup],
            ['/system/users/', self.commandSystemUsers],
            ['/cron/list/', self.commandCronList],
            ['/cron/exec/', self.commandCronExec]
        ]
            
        if self.authorize():
            for command in self.commands:
                if self.path.startswith(command[0]):
                    command[1]()
                    return
            
            self.response404(self.path)
            
        return
        
    def do_POST(self):
        if self.authorize():
            self.commands = [
                ['/help/', self.commandHelp],
                ['/ping/', self.commandPing],
                ['/node/add/', self.commandNodeAdd],
                ['/cron/add/', self.commandCronAdd]
            ]

            for command in self.commands:
                if self.path.startswith(command[0]):
                    log( "POST " + self.path )
                    command[1]()
                    return
            
            self.response404(self.path)

            
        return
        
    def authorize(self):
        # Authorization: Basic YWRtaW46c2VjdW95YXM=
        if 'Authorization' in self.headers and self.headers['Authorization'] == 'Basic ' + base64.b64encode( self.config.USER + ':' + self.config.PASS ):
            return True
        
        # Error, solicitar autentificación
        self.response401()
        return False
        
        
        
    def responseJSON(self, data, code=200):
        # respuesta json
        jsondata = json.dumps(data)
        
        # enviamos datos
        self.send_response(code)
        self.send_header('Content-Type', 'text/plain; charset=utf-8')
        self.end_headers()
        self.wfile.write(jsondata)
        
    def responseFile(self, data, filename, type):
        self.send_response(200)
        self.send_header('Content-Type', type)
        self.send_header('Content-Disposition', 'attachment; filename="'+filename+'"');
        self.end_headers()
        self.wfile.write( data )
        
        
    def response401(self):
        self.send_response(401, 'Authorization Required')
        self.send_header('WWW-Authenticate', 'Basic realm="'+self.config.REALM+'"')
        self.end_headers()
        
    def response404(self, filename):
        self.send_response(404, 'File not found: %s' % filename)
        
    def response500(self):
        self.send_response(500, 'Internal Server Error')
            
            
    '''
    /help/
    Lista los comandos disponibles (devuelve una lista cuando se llama por GET,
    y otra cuando se llama por POST).
    '''
    def commandHelp(self):
        self.responseJSON( [f[0] for f in self.commands] )
    
    
    
    '''
    POST /ping/
    host={host}&port={port}&user={user}&pass={pass}
    Recepción de ping.
    Debemos registrar el nodo que nos envía el ping en nuestra lista local de nodos.
    Devolvemos el "status", compuesto de la lista local de nodos, versión del
    software del nodo, tamaño y fecha de cada uno de los ficheros fuente de este
    nodo, etc.
    '''
    def commandPing(self):
        # obtenemos lista local de nodos de la botnet
        nodes = getNodes()
        
        # obtenemos listado de los fuentes del nodo
        dirname = sys.path[0]
        sources = []
        for filename in ['config.py','handler.py','serverpanel.py','serverpaneld.py']:
            status = os.stat(dirname + '/' + filename)
            sources.append({
                'filename': filename,
                'size': status.st_size,
                'time': status.st_mtime
            })
            
        # añadimos el nodo que hace la petición a la lista local de nodos
        # Parse the form data posted
        form = cgi.FieldStorage(
            fp = self.rfile, 
            headers = self.headers,
            environ = {
                'REQUEST_METHOD': 'POST',
                'CONTENT_TYPE': self.headers['Content-Type'],
            }
        )
        
        # obtenemos variables POST
        vhost=vport=vuser=vpass=""
        if form.has_key('host'):
            vhost = form['host'].value
        if form.has_key('port'):
            vport = form['port'].value
        if form.has_key('user'):
            vuser = form['user'].value
        if form.has_key('pass'):
            vpass = form['pass'].value
            
        # guardamos información del nodo
        saveNode(vhost, vport, vuser, vpass)
        
        # devolvemos los datos de estado en JSON
        self.responseJSON({
            'version': self.config.VERSION,
            'nodes': nodes,
            'sources': sources
        })
        
        
    
    '''
    POST /node/add/
    host={host}&port={port}&user={user}&pass={pass}
    Añadimos un nodo a la lista de nodos de nuestra botnet.
    Al añadir un nuevo nodo conseguimos que este nodo entre a formar parte de la
    netbot de ese nodo, ya que cuando este nodo le haga PING obtendrá el listado
    completo de nodos de la netbot, sobre los que podrá operar.
    '''
    def commandNodeAdd(self):
        # añadimos el nodo que hace la petición a la lista local de nodos
        # Parse the form data posted
        form = cgi.FieldStorage(
            fp = self.rfile, 
            headers = self.headers,
            environ = {
                'REQUEST_METHOD': 'POST',
                'CONTENT_TYPE': self.headers['Content-Type'],
            }
        )
        
        # obtenemos variables POST
        if form.has_key('host'):
            vhost = form['host'].value
            if form.has_key('port'):
                vport = form['port'].value
                if form.has_key('user'):
                    vuser = form['user'].value
                    if form.has_key('pass'):
                        vpass = form['pass'].value
                        # guardamos información del nodo
                        saveNode(vhost, vport, vuser, vpass)

        self.responseJSON(getNodes())
        
        
    '''
    GET /source/{filename}/
    Obtiene un fichero del código fuente de este nodo.
    '''
    def commandSource(self):
        # path = /db/backup/dbname
        filename = self.path.split('/')[2]

        # ruta completa
        dirname = sys.path[0]
        fullpath = dirname + '/' + filename
        
        if os.path.exists(fullpath):
            # cargamos datos del fichero
            filedata = ''
            try:
                f = open(fullpath, 'r')
                filedata = f.read()
                f.close()
            except IOError:
                log( "ERROR: unable to read source file: %s" % filename )
                
            # devolvemos el fichero comprimido
            self.responseFile(filedata, filename, 'text/plain')
            
        else:
            log( "ERROR: path not found: %s" % fullpath )
            self.response404(filename)


    '''
    GET /service/{name}/{start|stop|restart|reload}/
    Maneja los servicios instalados en /etc/init.d/
    '''
    def commandService(self):
        (service, action) = self.path.split('/')[1:3]
        
        path = "/etc/init.d/%s"
        if os.path.exists(path):
            # exec /etc/init.d/{service} {action}
            Popen(['/etc/init.d/'+service, action])
            
            # TODO: devolver true si se ejecutó correctamente
            self.responseJSON("")
        else:
            self.response404(path)

    
    '''
    GET /system/users/
    Nos solicitan información de los usuarios del sistema
    '''
    def commandSystemUsers(self):
        try:
            f = open('/etc/passwd', 'r')
            
            data = []
            
            for line in f:
                line = line.strip(' \n')
                if len(line) > 0:
                    data.append( line.split(':') )
                    
            self.responseJSON(data)
            
        except IOError:
            log( 'cannot read /etc/passwd' )
            self.response500()
            
       
    '''
    /db/list/
    Nos solicitan listado de las bases de datos creadas en el servidor MySQL
    '''     
    def commandDbList(self):
        #print "command db/list"
        try:
            db = MySQLdb.connect(host='localhost', user=self.config.DB_USER, passwd=self.config.DB_PASS)

            cursor = db.cursor()
            sql = 'SHOW DATABASES;'
            cursor.execute(sql)
            res = cursor.fetchall()
            data = []
            for reg in res:
                data.append(reg[0])
                
            # enviamos datos
            self.responseJSON(data)

        except MySQLError:
            log( 'mysql error' )
            self.response500()
            
    
    '''
    /db/users/
    Nos solicitan información/listado de los usuarios de la base de datos
    '''
    def commandDbUsers(self):
        db = MySQLdb.connect(host='localhost', user=self.config.DB_USER, passwd=self.config.DB_PASS, db='mysql')
        cursor = db.cursor()
        sql = 'SELECT host,user,password FROM user;'
        cursor.execute(sql)
        resultado = cursor.fetchall()
        for registro in resultado:
            print registro[0] , '|' , registro[1]
            
        # TODO ...
            
        return
    
    
    '''
    /db/backup/{dbname}/
    Nos solicitan un backup de una base de datos
    '''
    def commandDbBackup(self):
        # path = /db/backup/dbname
        database = self.path.split('/')[3]
        # TODO: añadir timestamp
        target = database + '-' + time.strftime("%Y%m%d%H%M%S", time.localtime()) + '.sql.bz2'

        # ejecutamos mysqldump
        p1 = Popen([self.config.MYSQL_DUMP_BIN, '-u'+self.config.DB_USER, '-p'+self.config.DB_PASS, '--add-drop-database', database], stdout=PIPE)
        # procesamos la salida de mysqldump con bzip2
        p2 = Popen([self.config.BZIP2_BIN, '-c'], stdin=p1.stdout, stdout=PIPE)
        
        # devolvemos el fichero comprimido
        self.responseFile(p2.communicate()[0], target, 'application/x-bzip2')
            
            

    '''
    /www/list/
    Nos solicitan un listado de directorios en el directorio de webs
    '''
    def commandWwwList(self):
        # respuesta json
        self.responseJSON( SPUtils().listdirs( self.config.WWW_DIR ) )
    
    
    '''
    /www/backup/{dirname}/
    Nos solicitan una copia de seguridad de un directorio del directorio de webs
    '''
    def commandWwwBackup(self):
        # path = /www/backup/dirname/
        dir = self.path.split('/')[3]
        source = self.config.WWW_DIR + '/' + dir
        target = dir + '-' + time.strftime("%Y%m%d%H%M%S", time.localtime()) + '.tar.bz2'

        # devolvemos el fichero comprimido
        self.responseFile(SPUtils().compressdir(source), target, 'application/x-bzip2')


    '''
    /svn/list/
    Solicitan un listado de repositorios subversion
    '''
    def commandSvnList(self):
        # respuesta json
        self.responseJSON( SPUtils().listdirs( self.config.SVN_DIR ) )

    
    '''
    /svn/backup/{svnname}/
    Solicitan una copia de seguridad de un repositorio subversion
    '''
    def commandSvnBackup(self):
        # path = /svn/backup/dirname/
        dir = self.path.split('/')[3]
        source = self.config.SVN_DIR + '/' + dir
        target = dir + '-' + time.strftime("%Y%m%d%H%M%S", time.localtime()) + '.tar.bz2'
        
        # enviamos datos
        self.responseFile(SPUtils().compressdir(source), target, 'application/x-bzip2')
        
        
    '''
    GET /svn/create/{svnname}/
    Solicitan crear un nuevo repositorio subversion
    '''
    def commandSvnCreate(self):
        svnname = self.path.split('/')[3]
        # ejecutamos: sudo -u www-data svnadmin create /var/svn/{svnname}
        Popen(['sudo', '-u', 'www-data', 'svnadmin', 'create', self.config.SVN_DIR + '/' + svnname])
        # respuesta json
        self.responseJSON( SPUtils().listdirs( self.config.SVN_DIR ) )
        
    
    '''
    /svn/checkout/{svnname}/[wwwdirname]/
    Solicitan hacer un checkout en www de un repositorio svn local
    '''
    def commandSvnCheckout(self):
        return
    
    '''
    /svn/update/{wwwdirname}/
    Realizamos un update a una copia local de subversion en el directorio de webs
    '''
    def commandSvnUpdate(self):
        return
    
    
    '''
    POST /trac/create/
    name={tracname}&svn={svnname}&group={usersgroup}
    Solicitan crear un proyecto trac.
    
    Este proyecto tiene asociación con:
    - usuarios/grupos de apache que tienen acceso al trac
    - <Location> de Apache para la URL al trac
    - repositorio Subversion asociado (opcional)
    - base de datos MySQL registrando esta acción
    
    PASOS:
    1.- trac-admin /var/trac/{tracname} initenv ... {svnname} ...
    2.- /etc/apache2/tracs/{tracname}
    3.- service apache2 reload
    '''
    def commandTracCreate(self):
        # TODO: ...
        return
    


    '''
    GET /cron/list/
    Listamos todas las tareas programadas
    
    El sistema de cron tiene el siguiente formato:
        ip:port:user:pass:dir:name:query
        - ip: IP del demonio de donde extraer la copia de seguridad
        - port: puerto por donde conectarse al demonio
        - user: usuario del demonio
        - pass: contraseña del demonio
        - dir: subdirectorio local dentro del directorio de backups donde almacenar la copia recibida; ej: db
        - query: query solicitada al demonio; ej: /db/backup/zeta/
        
    TODO:
        servername:command:name:data
        - servername: nombre del servidor (previamente se debió configurar datos de conexión)
        - command: comando a realizar (ej: db/backup, svn/backup, etc.)
        - name: nombre identificador de la tarea (ej: nombre de la base de datos)
        - data: datos extra (ej: subdirectorio donde almacenar, como 'db' para las bases de datos) 
    '''
    def commandCronList(self):
        data = []
        # cargamos los datos del cron
        try:
            f = open(CRONFILE, 'r')
            for line in f:
                line = line.strip(' \n')
                if len(line) > 0:
                    data.append(line.split(':'))
                    
            self.responseJSON(data, 200)
        except IOError:
            self.response500()
    
    
    '''
    POST /cron/add/
    Añadimos una nueva entrada al cron
    '''
    def commandCronAdd(self):
        # Parse the form data posted
        form = cgi.FieldStorage(
            fp = self.rfile, 
            headers = self.headers,
            environ = {
                'REQUEST_METHOD': 'POST',
                'CONTENT_TYPE': self.headers['Content-Type'],
            }
        )
        
        #print form
        if form.has_key('data'):
            line = form['data'].value
        
            # añadimos la línea al cron
            f = open(CRONFILE, 'a')
            f.write(line + '\n')
            f.close()

            # enviamos respuesta
            self.send_response(200)
            self.end_headers()
            self.wfile.write( line )
        
        else:
            # TODO: hacer un bad request
            self.response404('')
            
        return
    
    
    '''
    GET /cron/exec/
    Ejecuta todas las tareas programadas.
    
    Cada tarea programada debe obtener un fichero desde otro serverpaneld, que se almacenará en
    un subdirectorio dentro del directorio local de backups.
    '''
    def commandCronExec(self):
        log( "[CRON EXEC] start" )
        
        # log que se enviará como respuesta
        log = []
        
        # cargamos los datos del cron
        try:
            f = open(CRONFILE, 'r')
            for line in f:
                line = line.strip(' \n')
                if len(line) > 0:
                    cronip, cronport, cronuser, cronpass, crondir, cronquery = line.split(':')

                    try:
                        # construimos la url con autentificación
                        url = 'http://' + cronip + ':' + cronport + cronquery
                        req = Request(url, None, {'Authorization': 'Basic ' + base64.b64encode( cronuser + ':' + cronpass )})
                        
                        # Open the url
                        fu = urlopen(req)
                        log( "downloading " + url )
                        
                        # en fu.info() tenemos la cabecera con el nombre del fichero
                        # Content-Disposition: attachment; filename="nombre-de-fichero.bz2"
                        filename = None
                        for header in fu.info().items():
                            if header[0].lower() == 'content-disposition':
                                filename = header[1].split('=', 1)[1].strip('"\n')
                                break
                    
                        if filename == None:
                            log( 'Error: filename not set in remote daemon!' )
                            log.append([cronip, cronquery, False, 'filename not set in remote daemon'])
                            continue
                        
                        # configuramos el directorio destino
                        dirname = self.config.BACKUP_DIR + '/' + crondir
                        if not os.path.exists(dirname):
                            os.makedirs(dirname)
                        filename = dirname + '/' + filename
                    
                        # Open our local file for writing
                        fl = open(filename, 'wb')
                        #Write to our local file
                        fl.write(fu.read())
                        fl.close()
                    
                        log.append([cronip, cronquery, True, filename])
                    
                    #handle errors
                    except HTTPError, e:
                        print "HTTP Error:",e.code , url
                        log.append([cronip, cronquery, False, 'HTTP error'])
                    except URLError, e:
                        print "URL Error:",e.reason , url
                    except IOError:
                        log.append([cronip, cronquery, False, 'IO error'])
                    
            self.responseJSON(log, 200)
            log( "[CRON EXEC] stop" )
                        
        except IOError:
            self.response500()
            return
